package com.liebaut.util;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.HashMap;
import java.util.Map;

public class ReflectUtil {
    private ReflectUtil() {
    }

    public static ParameterizedType findParameterizedType(Class clazz) {
        while (clazz != null) {
            if (clazz.getGenericSuperclass() instanceof ParameterizedType) {
                return  ((ParameterizedType) clazz.getGenericSuperclass());
            }
            clazz = clazz.getSuperclass();
        }
        return null;
    }

    public static <A extends Annotation> A getInheritedAnnotation(Class<A> annotationClass, AnnotatedElement element) {
        A annotation = element.getAnnotation(annotationClass);
        if (annotation == null && element instanceof Method)
            annotation = getOverriddenAnnotation(annotationClass, (Method) element);
        return annotation;
    }

    private static <A extends Annotation> A getOverriddenAnnotation(Class<A> annotationClass, Method method) {
        final Class<?> methodClass = method.getDeclaringClass();
        final String name = method.getName();
        final Class<?>[] params = method.getParameterTypes();

        // prioritize all superclasses over all interfaces
        final Class<?> superclass = methodClass.getSuperclass();
        if (superclass != null) {
            final A annotation =
                    getOverriddenAnnotationFrom(annotationClass, superclass, name, params);
            if (annotation != null)
                return annotation;
        }

        // depth-first madresultxml over interface hierarchy
        for (final Class<?> intf : methodClass.getInterfaces()) {
            final A annotation =
                    getOverriddenAnnotationFrom(annotationClass, intf, name, params);
            if (annotation != null)
                return annotation;
        }

        return null;
    }

    private static <A extends Annotation> A getOverriddenAnnotationFrom(
            Class<A> annotationClass, Class<?> searchClass, String name, Class<?>[] params) {
        try {
            final Method method = searchClass.getMethod(name, params);
            final A annotation = method.getAnnotation(annotationClass);
            if (annotation != null)
                return annotation;
            return getOverriddenAnnotation(annotationClass, method);
        } catch (final NoSuchMethodException e) {
            return null;
        }
    }

    public static ParameterizedType getInheritedGenericReturnType(Method method) {
        return getInheritedGenericReturnType(method.getDeclaringClass(), method.getName(), method.getParameterTypes());
    }

    private static ParameterizedType getInheritedGenericReturnType(Class<?> clazz, String name, Class<?>[] params) {
        try {
            Method method = clazz.getMethod(name, params);
            if (method.getGenericReturnType() instanceof ParameterizedType)
                return (ParameterizedType) method.getGenericReturnType();
        } catch (final NoSuchMethodException e) {
            return null;
        }

        if (clazz.getSuperclass() != null)
            return getInheritedGenericReturnType(clazz.getSuperclass(), name, params);
        else
            return null;
    }

    private static Map<Class, Map<String, Field>> fieldsCache = new HashMap<Class, Map<String, Field>>();
    public static synchronized Map<String, Field> getAllFields(Class o) {
        Map<String, Field> map = fieldsCache.get(o);
        if (map == null) {
            map = new HashMap<String, Field>();
            Class clazz = o;
            while(clazz != null) {
                for(Field f : clazz.getDeclaredFields()) {
                    f.setAccessible(true);
                    map.put(f.getName(), f);
                }
                clazz = clazz.getSuperclass();
            }
            fieldsCache.put(o, map);
        }
        return map;
    }
}
